#include <iostream>
#include <string>
#include <sstream>
#include <iterator>
#include <list>
#include <algorithm>
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/regex.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/range/algorithm/count.hpp>
#include <boost/system/error_code.hpp>
#include <boost/chrono.hpp>
#include <system_error>

using namespace std;

int serverPort;
string configFile("server.cfg");
string calpuffExecutableLocation;
string calpuffOutputFilesLocation;
time_t calpuffVersion;

int numberOfFilesToProcess = 0;
int numberOfProcessedFiles = 0;

class FileToProcess
{
public:
	int numberOfSources;
	bool error; // określa czy wystąpił błąd
	string error_message; // treść błędu jeżeli error jest true
	boost::filesystem::path filePath;
	string folderName;

	FileToProcess() { }

	FileToProcess(string folderLocation)
	{
		this->error = false;
		boost::filesystem::path folderPath(folderLocation);
		if(! boost::filesystem::exists(folderPath))
		{//folder nie istnieje
			this->error = true;
			this->error_message = "Folder " + folderLocation + " nie istnieje.";
			return;
		}
		if(! boost::filesystem::is_directory(folderPath))
		{//podana ścieżka nie jest folderem
			this->error = true;
			this->error_message = folderLocation + " jest plikiem. Oczekiwano folderu.";
			return;
		}
		boost::filesystem::path filePath(folderPath / "BAEMARB.DAT");
		if(! boost::filesystem::exists(filePath))
		{//w folderze nie istnieje plik BAEMARB.DAT
			filePath = folderPath.parent_path() / "PTEMARB.DAT";
			if(! boost::filesystem::exists(filePath))
			{//w folderze nie istnieje plik PTEMARB.DAT
				this->error = true;
				this->error_message = "Brak BAEMARB.DAT lub PTEMARB.DAT w folderze " + folderLocation;
				return;
			}
		}
		boost::filesystem::ifstream fileStream(filePath); //otwiera plik z danymi do odczytu
		if(! fileStream.is_open())
		{
			this->error = true;
			this->error_message = filePath.string() + ": " + strerror(errno);
			return;
		}
		for(boost::filesystem::path::iterator iterator = folderPath.begin(); iterator != folderPath.end(); ++ iterator)
		{//wpisuje do folderName kolejne elementy ścieżki, w końcu trafia tam tylko nazwa folderu.
			this->folderName = (* iterator).string();
		}
		this->filePath = filePath;
		string fileHeader;
		getline(fileStream, fileHeader);
		if(fileStream.bad())
		{
			this->error = true;
			this->error_message = filePath.string() + ": " + strerror(errno);
			return;
		}
		fileStream.close();
		boost::regex sourcesRegex("^'.*'\\s+(\\d+).*"); // nagłówek pliku z danymi zawiera: 'nazwa_pliku'  liczbaźródeł, ten regeź wyciąga liczbę źródeł
		boost::smatch regexResults;
		if(boost::regex_match(fileHeader, regexResults, sourcesRegex))
		{
			this->numberOfSources = boost::lexical_cast<int>(regexResults[1]);
		}
		else
		{
			this->error = true;
			this->error_message = "Plik " + filePath.string() + " ma nieprawidlowy naglowek. Plik zostanie pominiety.";
			return;
		}
	}
};

bool parseConfigFile(string configFileLocation)
{
	boost::filesystem::path configFilePath(configFileLocation);
	boost::filesystem::ifstream configFileStream(configFilePath);
	if(! configFileStream.is_open())
	{
		perror(("[Error] " + configFileLocation).c_str());
		return false;
	}
	string configLine;
	boost::regex commentLineRegex("^#.*"); //wyłapuje linie zaczynające się znakiem #
	boost::regex validLineRegex("^\\s*(\\w+)\\s*=(.*)$"); //wyłapuje linie typu zmienna=wartość z ewentualnymi spacjami
	boost::smatch regexResults;
	while(getline(configFileStream, configLine))
	{
		boost::trim(configLine); // usuwa spacje z przodu i konca stringa
		if(configFileStream.bad())
		{
			perror(("[Error] " + configFileLocation).c_str());
			return false;
		}
		if(! configLine.empty() && ! boost::regex_match(configLine, regexResults, commentLineRegex))
		{
			if(boost::regex_match(configLine, regexResults, validLineRegex))
			{
				string variableName(regexResults[1]);
				if(strcmp(variableName.c_str(), "serverPort") == 0)
				{
					try
					{
						string portNumber = regexResults[2];
						boost::trim(portNumber);
						serverPort = boost::lexical_cast<int>(portNumber);
						if(serverPort < 1024 || serverPort > 65535)
						{
							cout << "[Error] " << configFileLocation << ": Wartosc podana dla serverPort \"" << regexResults[2] << "\" jest nieprawidlowa. Wartosc powinna byc z zakresu 1024-65535.\n";
							return false;
						}
					}
					catch(boost::bad_lexical_cast err)
					{
						cout << "[Error] " << configFileLocation << ": Wartosc podana dla serverPort \"" << regexResults[2] << "\" nie jest liczba\n";
						return false;
					}
				}
				else if(strcmp(variableName.c_str(), "calpuffExecutableLocation") == 0)
				{
					calpuffExecutableLocation = regexResults[2];
					boost::filesystem::path executablePath(calpuffExecutableLocation);
					if(! boost::filesystem::exists(executablePath))
					{
						cout << "[Error] " << configFileLocation << ": Sciezka podana dla calpuffExecutableLocation \"" << regexResults[2] << "\" jest nieprawidłowa lub plik nie istnieje.\n";
						return false;
					}
					else if(boost::filesystem::is_directory(executablePath))
					{
						cout << "[Error] " << configFileLocation << ": Sciezka podana dla calpuffExecutableLocation \"" << regexResults[2] << "\" jest folderem. Wymagana Sciezka do pliku.\n";
						return false;
					}
				}
				else if(strcmp(variableName.c_str(), "calpuffOutputFilesLocation") == 0)
				{
					calpuffOutputFilesLocation = regexResults[2];
					boost::filesystem::path testPath(calpuffOutputFilesLocation);
					if(! boost::filesystem::exists(testPath))
					{
						boost::filesystem::create_directories(testPath);
					}
				}
			}
			//jeżeli linia zawiera co co nie jest przypisaniem zmienna= wartosc nic nie rób
		}
		//jezeli linia jest pusta albo jest komentarzem nic nie rob
	}
	configFileStream.close();

	return true;
}

bool getListOfFilesToProcess(string fileName, list<FileToProcess> &files)
{
	string folderLocation;
	boost::filesystem::path filePath(fileName);
	boost::filesystem::ifstream fileStream(filePath);
	if(! fileStream.is_open())
	{
		perror(("[Error] " + filePath.string()).c_str());
		return false;
	}
	while(getline(fileStream, folderLocation))
	{
		if(fileStream.bad())
		{
			perror(("[Error] " + filePath.string()).c_str());
			return false;
		}
		FileToProcess file(folderLocation);
		if(file.error)
		{
			printf("[Warning] %s: %s\n", fileName.c_str(), file.error_message.c_str());
			continue;
		}
		numberOfFilesToProcess ++;
		if(files.empty())
		{
			files.push_back(file);
		}
		else
		{
			list<FileToProcess>::iterator listIterator = files.begin();
			while(listIterator != files.end())
			{
				if(listIterator->numberOfSources <= file.numberOfSources)
				{
					files.insert(listIterator, file);
					break;
				}
				else if(++ listIterator == files.end())
				{
					files.push_back(file);
				}
			}
		}
	}
	fileStream.close();

	return true;
}

string getFileFromTcpStream(string fileLocation, boost::asio::ip::tcp::iostream &tcpStream)
{
	string lineFromStream;
	getline(tcpStream, lineFromStream); //pobieranie nazwy pliku
	if(tcpStream.bad())
	{
		perror("[Error] Blad odczytu nazwy pliku");
	}
	boost::filesystem::path filePath(fileLocation);
	if(boost::filesystem::is_directory(filePath))
	{
		filePath /= lineFromStream;
	}
	if(! boost::filesystem::exists(filePath.parent_path()))
	{
		boost::filesystem::create_directories(filePath.parent_path());
	}
	boost::filesystem::ofstream fileStream(filePath);
	if(! fileStream.is_open())
	{
		perror(("[Error] " + filePath.string()).c_str());
	}
	getline(tcpStream, lineFromStream); //pobieranie rozmiaru pliku
	int fileSize = boost::lexical_cast<int>(lineFromStream);
	if(tcpStream.bad())
	{
		perror("[Error] Blad odczytu rozmiaru pliku");
	}
	int bufferSize = 2097152; //2MB
	char buffer[bufferSize];
	int sizeToRead = 0;
	while(1)
	{
		if(fileSize > bufferSize)
		{
			sizeToRead = bufferSize;
		}
		else
		{
			sizeToRead = fileSize;
		}
		tcpStream.read(buffer, sizeToRead);

		if(tcpStream.bad())
		{
			perror("[Error] Blad odczytu pliku z serwera");
		}
		fileStream.write(buffer, sizeToRead);
		if(fileStream.bad())
		{
			perror(("[Error] Blad zapisu do pliku " + filePath.string()).c_str());
		}
		fileStream.flush();
		fileSize -= sizeToRead;
		if(fileSize == 0)
		{//jezeli caly plik został odczytany, przerwij petle
			break;
		}
	}
	fileStream.close();
	//potwierdzenie odczytania całego pliku
	tcpStream << "OK\n";
	tcpStream << flush;
	return filePath.string();
}

bool sendFileWithTcpStream(string fileLocation, boost::asio::ip::tcp::iostream &tcpStream)
{
	boost::filesystem::path filePath(fileLocation);
	streamsize fileSize(boost::filesystem::file_size(filePath));
	boost::filesystem::ifstream fileStream(filePath, std::ios::in | std::ios::binary);
	if(! fileStream.is_open())
	{
		perror("[Error] ");
		fileStream.close();
		return false;
	}
	tcpStream << filePath.filename().string() << "\n";
	tcpStream << fileSize << "\n";
	boost::array<char, 1024> buffer;
	boost::system::error_code error;
	for(;;)
	{
		if(fileStream.eof() == false)
		{
			fileStream.read(buffer.c_array(), (std::streamsize)buffer.size());
			if(fileStream.gcount() <= 0)
			{
				cout << "[Error] Blad odczytu " << fileLocation << "\n";
				fileStream.close();
				return false;
			}
			tcpStream.write(buffer.c_array(), fileStream.gcount());
			if(error)
			{
				cout << "[Error] Blad wysyłania: " << error.message() << std::endl;
				fileStream.close();
				return false;
			}
		}
		else
		{
			break;
		}
	}
	fileStream.close();
	tcpStream << flush;
	//przed kontynuacja serwer czeka na potwierdzenie od klienta
	string tmp;
	tcpStream >> tmp;
	return true;
}

int main(int argc, char* argv[])
{

	boost::chrono::system_clock::time_point start = boost::chrono::system_clock::now();
	boost::system::error_code error;
	boost::filesystem::path calpuffExecutablePath(calpuffExecutableLocation);
	time_t CalpuffLastModified = boost::filesystem::last_write_time(calpuffExecutablePath, error);
	if(! error)
	{
		calpuffVersion = CalpuffLastModified;
	}
	else
	{
		calpuffVersion = 0;
	}
	//parsowanie server.cfg
	if(! parseConfigFile(configFile))
	{
		printf("[Error] Blad podczas interpretacji pliku: %s", configFile.c_str());
		return 1;
	}
	//pobieranie listy folderów z pliku
	list<FileToProcess> FilesToProcess;
	if(argc != 2)
	{
		printf("Serwer wymaga parametru: ./server files.txt\nfiles.txt powinien zaiwerac liste sciezek do folderow do przetworzenia. Jedna sciezka w lini.");
		return 1;
	}
	if(argc == 2)
	{
		if(! getListOfFilesToProcess(argv[1], FilesToProcess))
		{
			printf("[Error] Blad podczas interpretacji pliku: %s", configFile.c_str());
		}
	}

	//połączenie z portem i oczekiwanie na połączenia
	using boost::asio::ip::tcp;
	try
	{
		boost::asio::io_service io_service;
		tcp::endpoint endpoint(tcp::v4(), serverPort);
		tcp::acceptor acceptor(io_service, endpoint);
		printf("Oczekiwanie na polaczenia na porcie %d...\n", serverPort);
		while(true)
		{
			tcp::iostream tcpStream;
			string request;
			boost::regex completedRegex("^completed: (.+)$");
			boost::smatch regexResults;
			boost::system::error_code errorCode;
			acceptor.accept(*tcpStream.rdbuf(), errorCode);
			if(! errorCode)
			{
				getline(tcpStream, request);
				if(tcpStream.bad())
				{
					perror("[Error] tcp stream");
					continue;
				}
				if(strcmp(request.c_str(), "get file") == 0)
				{
					if(! FilesToProcess.empty())
					{
						FileToProcess file = FilesToProcess.front();
						FilesToProcess.pop_front();
						tcpStream << file.folderName << "\n";
						if(! sendFileWithTcpStream(file.filePath.string(), tcpStream))
						{
							tcpStream.close();
							continue;
						}
						//Plik CALPUFF.INP powinien być w folderze nadrzędnym
						boost::filesystem::path inpPath = file.filePath.parent_path().parent_path() / "CALPUFF.INP";
						if(! sendFileWithTcpStream(inpPath.string(), tcpStream))
						{
							tcpStream.close();
							continue;
						}
						tcpStream << file.numberOfSources << "\n";
						tcpStream.close();
					}
					else
					{
						tcpStream << "exit\n"; // nie ma więcej plików do przetworzenia, klient powinien zakończyć działanie.
						tcpStream.close();
					}
				}
				else if(boost::regex_match(request, regexResults, completedRegex))
				{
					numberOfProcessedFiles ++;
					string folderName = regexResults[1];
					boost::trim(folderName);
					cout << "Przetworzono " << numberOfProcessedFiles << " z " << numberOfFilesToProcess << " plikow\n";
					boost::filesystem::path outputFolderPath(calpuffOutputFilesLocation);
					outputFolderPath /= folderName;
					if(! boost::filesystem::exists(outputFolderPath))
					{
						boost::filesystem::create_directories(outputFolderPath);
					}
					getFileFromTcpStream(outputFolderPath.string(), tcpStream);
					getFileFromTcpStream(outputFolderPath.string(), tcpStream);
					if(numberOfProcessedFiles == numberOfFilesToProcess)
					{//jeżeli przetworzono wszystkie pliki
						tcpStream << "exit\n";
						printf("[Success] Pliki zostaly przetworzone.\n");
						boost::chrono::duration<double> timer = boost::chrono::system_clock::now() - start;
						double seconds = timer.count();
						int hours = (int) seconds / 3600;
						seconds -= hours * 3600;
						int minutes = (int) seconds / 60;
						seconds -= minutes * 60;
						std::cout << "Czas obliczen: " << hours << " h " << minutes << " m " << (int) seconds << " s\n";
						return 0;
					}
					else
					{
						tcpStream << "continue\n";
					}
				}
				else if(strcmp(request.c_str(), "get calpuff version") == 0)
				{
					tcpStream << calpuffVersion;
				}
				else if(strcmp(request.c_str(), "get calpuff") == 0)
				{
					if(! sendFileWithTcpStream(calpuffExecutableLocation, tcpStream))
					{
						tcpStream.close();
						continue;
					}
				}
			}
			else
			{
				printf("[Error] : %s\n", errorCode.message().c_str());
			}
		}
	}
	catch(std::exception& e)
	{
		printf("[Error] :%s\n", e.what());
		return 1;
	}
}